Skip to content

Look a wrapper class up without boxing its class id - #2960

Merged
pyricau merged 1 commit into
mainfrom
strip-without-boxing-class-ids
Aug 21, 2026
Merged

Look a wrapper class up without boxing its class id#2960
pyricau merged 1 commit into
mainfrom
strip-without-boxing-class-ids

Conversation

@pyricau

@pyricau pyricau commented Aug 20, 2026

Copy link
Copy Markdown
Member

While benchmarking HprofPrimitiveArrayStripper against hprof-redact I noticed the bytes it allocates grow with the heap dump, and traced it to one boxed Long per record.

What it was

Whether an instance wraps a primitive is a lookup by class id, and that lookup went to a Map<Long, PrimitiveWrapperClass>. Hashing a Long key boxes it, and class ids are far outside Integer/Long's valueOf cache, so every instance dump, LOAD_CLASS and CLASS_DUMP record allocated a 24 byte Long that died immediately.

JFR names the site directly — Long.valueOf under HprofPrimitiveArrayStripper.stripPrimitiveArrays, 41.9 MB of the 49.8 MB one strip allocated — and counting the records that reach a lookup accounts for the total to within 224 bytes:

heap dump instance dumps + LOAD_CLASS + CLASS_DUMP × 24 bytes measured, minus what's left after the fix
1.4 GB, taken from a JVM 17,964,631 431,151,144 431,150,920

What it is now

There are only ever 8 primitive wrapper classes, so the ids found so far are held in a LongArray and scanned. Scanning ≤ 8 longs is cheaper than hashing one, and it allocates nothing.

heap dump allocated before after
leak_asynctask_o.hprof, 8 MB, 65,814 instances 5.2 MB 3.5 MB
compose_leak.hprof, 25 MB, 152,199 instances 9.8 MB 5.8 MB
294 MB, 1,154,587 instances 33.9 MB 5.8 MB
1.4 GB, 17,961,453 instances 417 MB 5.8 MB

Allocation no longer grows with the heap dump. Wall clock is unchanged (2.92–2.99 s vs 2.94–3.08 s on the 1.4 GB dump) — bump allocation is cheap and these objects died young — so the win is GC pressure, which is what matters on Android, where HeapAnalysisConfig(stripHeapDump = true) strips inside the app whose heap was just dumped.

Correctness

Output is byte for byte identical before and after on six heap dumps: leak_asynctask_o.hprof, leak_asynctask_m.hprof and compose_leak.hprof from our test resources, plus three taken from a JVM, covering both identifier sizes. The existing stripper tests, detekt and checkKotlinAbi pass; the new class is private, so there's no ABI change.

No new test, deliberately

A regression here is invisible: the output stays correct and only the garbage comes back. The test that would catch it has to measure allocation, and the measurement is JIT sensitive — C2's escape analysis scalar replaces this very box during a JVM's first strip and stops doing so afterwards, which is what made the first round of every benchmark read 36 MB instead of 437 MB. A test would need a warm-up strip and would still read differently under -XX:TieredStopAtLevel=1 or -XX:-DoEscapeAnalysis. Happy to add one if you'd rather have it with those caveats.

🤖 Generated with Claude Code

@pyricau
pyricau force-pushed the strip-without-boxing-class-ids branch from 70bf737 to a26aeea Compare August 20, 2026 14:52
Whether an instance wraps a primitive is a lookup by class id, and that
lookup went to a Map<Long, PrimitiveWrapperClass>, so hashing the key
boxed the id of every instance dump, LOAD_CLASS and CLASS_DUMP record.
That is 24 bytes per record: 431 MB of the 437 MB that stripping a
1.4 GB heap dump of 17961453 instances allocated.

There's one primitive wrapper class per primitive type, so the ids fit
in an 8 long array that is cheaper to scan than to hash. What stripping
allocates no longer grows with the heap dump, which matters on Android,
where stripping runs inside the app whose heap was just dumped.

Output is byte for byte identical on the Android heap dumps in our test
resources and on heap dumps taken from a JVM with both identifier sizes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pyricau
pyricau force-pushed the strip-without-boxing-class-ids branch from a26aeea to 7340b35 Compare August 21, 2026 13:52
@pyricau
pyricau merged commit e24e8ae into main Aug 21, 2026
14 checks passed
@pyricau
pyricau deleted the strip-without-boxing-class-ids branch August 21, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant